package com.boardgamegeek.util; import android.text.Editable; import android.text.Html; import android.text.Spanned; import android.text.style.BulletSpan; import android.text.style.LeadingMarginSpan; import org.xml.sax.XMLReader; import java.util.Stack; import timber.log.Timber; /** * A {@link android.text.Html.TagHandler} that can format ordered and unordered lists. Adapted from: * https://bitbucket.org/Kuitsi/android-textview-html-list */ public class ListTagHandler implements Html.TagHandler { private static final String UL = "ul"; private static final String OL = "ol"; private static final String LI = "li"; // List indentation in pixels. Nested lists use multiple of this. private static final int INDENTATION_IN_PIXELS = 10; private static final int LIST_ITEM_INDENTATION_IN_PIXELS = INDENTATION_IN_PIXELS * 2; private static final BulletSpan BULLET_SPAN = new BulletSpan(INDENTATION_IN_PIXELS); // Keeps track of lists (ol, ul). On bottom of Stack is the outermost list and on top of Stack is the most nested private final Stack<String> lists = new Stack<>(); // Tracks indexes of ordered lists so that after a nested list ends we can continue with correct index of outer list private final Stack<Integer> nextOrderedIndex = new Stack<>(); private static void startListItem(Editable text, Object type) { int length = text.length(); text.setSpan(type, length, length, Spanned.SPAN_MARK_MARK); } private static void endListItem(Editable text, Class<?> kind, Object... replaces) { int length = text.length(); Object lastSpan = getLastSpan(text, kind); int where = text.getSpanStart(lastSpan); text.removeSpan(lastSpan); if (where != length) { for (Object replace : replaces) { text.setSpan(replace, where, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private static Object getLastSpan(Spanned text, Class<?> kind) { /* * This knows that the last returned object from getSpans() will be the most recently added. */ Object[] spans = text.getSpans(0, text.length(), kind); if (spans.length == 0) { return null; } return spans[spans.length - 1]; } @Override public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { if (tagIsTypeOf(tag, UL)) { if (opening) { lists.push(tag); } else { lists.pop(); } } else if (tagIsTypeOf(tag, OL)) { if (opening) { lists.push(tag); nextOrderedIndex.push(1); } else { lists.pop(); nextOrderedIndex.pop(); } } else if (tagIsTypeOf(tag, LI)) { String currentListTag = lists.peek(); ensureTrailingNewLine(output); if (opening) { if (tagIsTypeOf(currentListTag, UL)) { startListItem(output, new Ul()); } else if (tagIsTypeOf(currentListTag, OL)) { startListItem(output, new Ol()); output.append(nextOrderedIndex.peek().toString()).append(". "); nextOrderedIndex.push(nextOrderedIndex.pop() + 1); } } else { if (tagIsTypeOf(currentListTag, UL)) { // Nested BulletSpans increases distance between bullet and text, so we must prevent it. int margin = INDENTATION_IN_PIXELS; if (lists.size() > 1) { margin = INDENTATION_IN_PIXELS - BULLET_SPAN.getLeadingMargin(true); if (lists.size() > 2) { // This gets more complicated when we add a LeadingMarginSpan into the same line: // we have also counter it's effect to BulletSpan margin -= (lists.size() - 2) * LIST_ITEM_INDENTATION_IN_PIXELS; } } BulletSpan newBullet = new BulletSpan(margin); endListItem(output, Ul.class, new LeadingMarginSpan.Standard(LIST_ITEM_INDENTATION_IN_PIXELS * (lists.size() - 1)), newBullet); } else if (tagIsTypeOf(currentListTag, OL)) { int margin = LIST_ITEM_INDENTATION_IN_PIXELS * (lists.size() - 1); if (lists.size() > 2) { // Same as in ordered lists: counter the effect of nested Spans margin -= (lists.size() - 2) * LIST_ITEM_INDENTATION_IN_PIXELS; } endListItem(output, Ol.class, new LeadingMarginSpan.Standard(margin)); } } } else { if (opening) { Timber.d("Found an unsupported tag: %s", tag); } } } private boolean tagIsTypeOf(String tag, String type) { return tag.equalsIgnoreCase(type); } private void ensureTrailingNewLine(Editable line) { if (line.length() > 0 && line.charAt(line.length() - 1) != '\n') { line.append("\n"); } } private static class Ul { } private static class Ol { } }